﻿using StableDiffusionTools.Interop;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using System.Threading.Tasks;
using static StableDiffusionTools.Utils.NativeMethods;

namespace StableDiffusionTools.Utils
{
    public static class ExplorerUtil
    {
        private static Guid IID_IShellFolder = typeof(IShellFolder).GUID;
        private static readonly int pointerSize = Marshal.SizeOf(typeof(IntPtr));

        public static bool OpenWithPictureViewer(string path)
        {
            if (string.IsNullOrEmpty(path)) return false;

            Process proc = new Process();
            ProcessStartInfo startInfo = new ProcessStartInfo();
            string systemPath = Environment.GetFolderPath(Environment.SpecialFolder.System);
            string rundllPath = Path.Combine(systemPath, "rundll32.exe");
            string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
            string photoViewer = Path.Combine(programFiles, @"Windows Photo Viewer\PhotoViewer.dll");
            string arguments = string.Format("\"{0}\", ImageView_Fullscreen {1}", photoViewer, path);
            startInfo.FileName = rundllPath;
            startInfo.Arguments = arguments;
            proc.StartInfo = startInfo;
            bool success = true;
            try
            {
                proc.Start();
            }
            catch (Exception)
            {
                success = false;
            }

            return success;
        }

        public static bool Open(string path, bool openExisting = false)
        {
            if (string.IsNullOrEmpty(path)) return false;

            PathInfo pathInfo = ExtendedPathInfo.GetPathInfo(path);
            if (pathInfo == PathInfo.File)
            {
                if (openExisting)
                    OpenExistingExplorer(new FileInfo(path));
                else
                    Shell32.ShellExecute(IntPtr.Zero, "open", "explorer.exe", string.Format("/e, /select, \"{0}\"", path), "", ShowCommands.SW_NORMAL);
            }
            else if (pathInfo == PathInfo.Directory)
            {
                if (openExisting)
                    Shell32.ShellExecute(IntPtr.Zero, "explore", path, "", "", ShowCommands.SW_NORMAL);
                else
                    Shell32.ShellExecute(IntPtr.Zero, "open", "explorer.exe", string.Format("/e, \"{0}\"", path), "", ShowCommands.SW_NORMAL);

            }

            return pathInfo != PathInfo.NotFound;
        }

        public static void OpenRange(string path) => OpenRange(new string[] { path });
        public static void OpenRange(IEnumerable<string> paths)
        {
            if (paths == null) return;

            Dictionary<string, List<string>> DirectoryFiles = new Dictionary<string, List<string>>();

            foreach (var path in paths)
            {
                var info = ExtendedPathInfo.GetPathInfo(path);
                if (info == PathInfo.File)
                {
                    FileInfo pathInfo = new FileInfo(path);
                    string directory = pathInfo.DirectoryName;
                    string name = pathInfo.Name;
                    if (!DirectoryFiles.TryGetValue(directory, out List<string> filesList))
                        DirectoryFiles[directory] = filesList = new List<string>();

                    filesList.Add(name);
                }
                else if (info == PathInfo.Directory)
                {
                    DirectoryInfo pathInfo = new DirectoryInfo(path);
                    string directory = pathInfo.Parent?.FullName;
                    if (directory != null) //could be root path, if so ignore it
                    {
                        string name = pathInfo.Name;
                        if (!DirectoryFiles.TryGetValue(directory, out List<string> filesList))
                        {
                            filesList = new List<string>();
                            DirectoryFiles[directory] = filesList;
                        }

                        filesList.Add(name);
                    }
                }
            }

            foreach (var dir in DirectoryFiles)
                OpenExistingExplorer(dir.Key, dir.Value);
        }

        private static void OpenExistingExplorer(FileInfo file) => OpenExistingExplorer(file.DirectoryName, new string[] { file.Name });
        private static void OpenExistingExplorer(string path, string filePath) => OpenExistingExplorer(path, new string[] { filePath });
        private static void OpenExistingExplorer(string path, IEnumerable<string> paths)
        {
            IntPtr folderPIDL = IntPtr.Zero;
            IShellFolder folder;
            List<IntPtr> filePIDLs = new List<IntPtr>();
            IntPtr PIDLPointersArray = IntPtr.Zero;
            try
            {
                folderPIDL = ILCreateFromPath(path);
                folder = PIDLToShellFolder(folderPIDL);

                foreach (var item in paths)
                {
                    filePIDLs.Add(GetShellFolderChildrenPIDL(folder, item));
                }

                PIDLPointersArray = Marshal.AllocHGlobal(pointerSize * filePIDLs.Count);
                for (int i = 0; i < filePIDLs.Count; i++)
                {
                    Marshal.WriteIntPtr(PIDLPointersArray, i * pointerSize, filePIDLs[i]);
                }
                Marshal.ThrowExceptionForHR(SHOpenFolderAndSelectItems_(folderPIDL, (uint)filePIDLs.Count, PIDLPointersArray, 0));
            }
            finally
            {
                if (PIDLPointersArray != IntPtr.Zero) Marshal.FreeHGlobal(PIDLPointersArray);

                foreach (var filePIDL in filePIDLs)
                    ILFree(filePIDL);

                if (folderPIDL != IntPtr.Zero) ILFree(folderPIDL);
            }
        }

        private static IntPtr GetShellFolderChildrenPIDL(IShellFolder parentFolder, string displayName)
        {
            var bindCtx = CreateBindCtx();

            uint pdwAttributes = 0;
            parentFolder.ParseDisplayName(IntPtr.Zero, null, displayName, out uint pchEaten, out IntPtr ppidl, ref pdwAttributes);

            return ppidl;
        }

        //private static IntPtr SHSimpleIDListFromPath(string path) => GetShellFolderChildrenPIDL(SHGetDesktopFolder(), path);

        private static IShellFolder PIDLToShellFolder(IntPtr pidl) => PIDLToShellFolder(SHGetDesktopFolder(), pidl);

        private static IShellFolder PIDLToShellFolder(IShellFolder parent, IntPtr pidl)
        {
            Marshal.ThrowExceptionForHR((int)parent.BindToObject(pidl, IntPtr.Zero, ref IID_IShellFolder, out IShellFolder folder));
            return folder;
        }
    }

    internal class NativeMethods
    {
        [DllImport("ole32.dll", EntryPoint = "CreateBindCtx")]
        internal static extern int CreateBindCtx_(int reserved, out IBindCtx ppbc);

        internal static IBindCtx CreateBindCtx()
        {
            IBindCtx result;
            Marshal.ThrowExceptionForHR(CreateBindCtx_(0, out result));
            return result;
        }

        [DllImport("shell32.dll", EntryPoint = "SHGetDesktopFolder", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern int SHGetDesktopFolder_([MarshalAs(UnmanagedType.Interface)] out IShellFolder ppshf);

        internal static IShellFolder SHGetDesktopFolder()
        {
            IShellFolder result;
            Marshal.ThrowExceptionForHR(SHGetDesktopFolder_(out result));
            return result;
        }

        [DllImport("shell32.dll", EntryPoint = "SHOpenFolderAndSelectItems")]
        internal static extern int SHOpenFolderAndSelectItems_([In] IntPtr pidlFolder, uint cidl, [In, Optional] IntPtr apidl, int dwFlags);

        [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
        internal static extern IntPtr ILCreateFromPath([In, MarshalAs(UnmanagedType.LPWStr)] string pszPath);

        [DllImport("shell32.dll")]
        internal static extern void ILFree([In] IntPtr pidl);

        internal static IntPtr GetShellFolderChildrenPIDL(IShellFolder parentFolder, string displayName)
        {
            var bindCtx = CreateBindCtx();

            uint pchEaten;
            uint pdwAttributes = 0;
            IntPtr ppidl;
            parentFolder.ParseDisplayName(IntPtr.Zero, null, displayName, out pchEaten, out ppidl, ref pdwAttributes);

            return ppidl;
        }

        internal static IntPtr SHSimpleIDListFromPath(string path)
        {
            var desktopFolder = SHGetDesktopFolder();
            return GetShellFolderChildrenPIDL(desktopFolder, path);
        }

        internal static Guid IID_IShellFolder = typeof(IShellFolder).GUID;
        internal static int pointerSize = Marshal.SizeOf(typeof(IntPtr));

        internal static IShellFolder PIDLToShellFolder(IShellFolder parent, IntPtr pidl)
        {
            IShellFolder folder;
            var result = parent.BindToObject(pidl, IntPtr.Zero, ref IID_IShellFolder, out folder);
            Marshal.ThrowExceptionForHR((int)result);
            return folder;
        }

        internal static IShellFolder PIDLToShellFolder(IntPtr pidl)
        {
            return PIDLToShellFolder(SHGetDesktopFolder(), pidl);
        }

        [ComImport,
        Guid("000214E6-0000-0000-C000-000000000046"),
        InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
        ComConversionLoss]
        internal interface IShellFolder
        {
            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            void ParseDisplayName(IntPtr hwnd, [In, MarshalAs(UnmanagedType.Interface)] IBindCtx pbc, [In, MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName, [Out] out uint pchEaten, [Out] out IntPtr ppidl, [In, Out] ref uint pdwAttributes);
            [PreserveSig]
            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            HResult EnumObjects([In] IntPtr hwnd, [In] ShellFolderEnumerationOptions grfFlags, [MarshalAs(UnmanagedType.Interface)] out IEnumIDList ppenumIDList);

            [PreserveSig]
            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            HResult BindToObject([In] IntPtr pidl, /*[In, MarshalAs(UnmanagedType.Interface)] IBindCtx*/ IntPtr pbc, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IShellFolder ppv);

            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            void BindToStorage([In] ref IntPtr pidl, [In, MarshalAs(UnmanagedType.Interface)] IBindCtx pbc, [In] ref Guid riid, out IntPtr ppv);

            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            void CompareIDs([In] IntPtr lParam, [In] ref IntPtr pidl1, [In] ref IntPtr pidl2);

            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            void CreateViewObject([In] IntPtr hwndOwner, [In] ref Guid riid, out IntPtr ppv);

            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            void GetAttributesOf([In] uint cidl, [In] IntPtr apidl, [In, Out] ref uint rgfInOut);


            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            void GetUIObjectOf([In] IntPtr hwndOwner, [In] uint cidl, [In] IntPtr apidl, [In] ref Guid riid, [In, Out] ref uint rgfReserved, out IntPtr ppv);

            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            void GetDisplayNameOf([In] ref IntPtr pidl, [In] uint uFlags, out IntPtr pName);

            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            void SetNameOf([In] IntPtr hwnd, [In] ref IntPtr pidl, [In, MarshalAs(UnmanagedType.LPWStr)] string pszName, [In] uint uFlags, [Out] IntPtr ppidlOut);
        }

        [ComImport,
        Guid("000214F2-0000-0000-C000-000000000046"),
        InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        internal interface IEnumIDList
        {
            [PreserveSig]
            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            HResult Next(uint celt, IntPtr rgelt, out uint pceltFetched);

            [PreserveSig]
            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            HResult Skip([In] uint celt);

            [PreserveSig]
            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            HResult Reset();

            [PreserveSig]
            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            HResult Clone([MarshalAs(UnmanagedType.Interface)] out IEnumIDList ppenum);
        }




        [Flags]
        internal enum ShellFolderEnumerationOptions : ushort
        {
            CheckingForChildren = 0x0010,
            Folders = 0x0020,
            NonFolders = 0x0040,
            IncludeHidden = 0x0080,
            InitializeOnFirstNext = 0x0100,
            NetPrinterSearch = 0x0200,
            Shareable = 0x0400,
            Storage = 0x0800,
            NavigationEnum = 0x1000,
            FastItems = 0x2000,
            FlatList = 0x4000,
            EnableAsync = 0x8000
        }

        internal enum HResult
        {
            /// <summary>     
            /// S_OK          
            /// </summary>    
            Ok = 0x0000,

            /// <summary>
            /// S_FALSE
            /// </summary>        
            False = 0x0001,

            /// <summary>
            /// E_INVALIDARG
            /// </summary>
            InvalidArguments = unchecked((int)0x80070057),

            /// <summary>
            /// E_OUTOFMEMORY
            /// </summary>
            OutOfMemory = unchecked((int)0x8007000E),

            /// <summary>
            /// E_NOINTERFACE
            /// </summary>
            NoInterface = unchecked((int)0x80004002),

            /// <summary>
            /// E_FAIL
            /// </summary>
            Fail = unchecked((int)0x80004005),

            /// <summary>
            /// E_ELEMENTNOTFOUND
            /// </summary>
            ElementNotFound = unchecked((int)0x80070490),

            /// <summary>
            /// TYPE_E_ELEMENTNOTFOUND
            /// </summary>
            TypeElementNotFound = unchecked((int)0x8002802B),

            /// <summary>
            /// NO_OBJECT
            /// </summary>
            NoObject = unchecked((int)0x800401E5),

            /// <summary>
            /// Win32 Error code: ERROR_CANCELLED
            /// </summary>
            Win32ErrorCanceled = 1223,

            /// <summary>
            /// ERROR_CANCELLED
            /// </summary>
            Canceled = unchecked((int)0x800704C7),

            /// <summary>
            /// The requested resource is in use
            /// </summary>
            ResourceInUse = unchecked((int)0x800700AA),

            /// <summary>
            /// The requested resources is read-only.
            /// </summary>
            AccessDenied = unchecked((int)0x80030005)
        }
    }
}
